package mqtt

import (
	"encoding/json"
	"fmt"
	"strconv"

	"github.com/project-flogo/core/data/coerce"
)

// Settings trigger settings
type Settings struct {
	Name                 string                 `md:"name"`
	Servers              []interface{}          `md:"servers,required"` // The broker URL
	ClientID             string                 `md:"clientId"`         // The id of client
	Username             string                 `md:"username"`         // The user's name
	Password             string                 `md:"password"`         // The user's password
	CleanSession         bool                   `md:"cleanSession"`     // Clean session flag
	Order                bool                   `md:"order"`
	WillEnabled          bool                   `md:"willEnabled"`
	WillTopic            string                 `md:"willTopic"`
	WillPayload          []byte                 `md:"willPayload"`
	WillQos              int                    `md:"willQos"`
	WillRetained         bool                   `md:"willRetained"`
	ProtocolVersion      int                    `md:"protocolVersion"`
	TLSConfig            map[string]interface{} `md:"tlsConfig"`
	KeepAlive            string                 `md:"keepAlive"` // Keep Alive time in seconds
	PingTimeout          string                 `md:"pingTimeout"`
	ConnectTimeout       string                 `md:"connectTimeout"`
	MaxReconnectInterval string                 `md:"maxReconnectInterval"`
	AutoReconnect        bool                   `md:"autoReconnect"` // Enable Auto-Reconnect
	Store                string                 `md:"store"`         // The store for message persistence
	WriteTimeout         string                 `md:"writeTimeout"`
	ResumeSubs           bool                   `md:"resumeSubs"`
	HTTPHeaders          map[string]string      `md:"httpHeaders"`
	// SSLConfig     map[string]interface{} `md:"sslConfig"`         // SSL Configuration
}

// HandlerSettings handler settings
type HandlerSettings struct {
	Kind       string `md:"kind"`
	Topic      string `md:"topic"`      // The topic to listen on
	ReplyTopic string `md:"replyTopic"` // The topic to reply on
	Qos        int    `md:"qos"`        // The Quality of Service
}

type clientOptions struct {
	Servers              []string
	ResumeSubs           bool
	ClientID             string
	Username             string
	Password             string
	CleanSession         bool
	Order                bool
	WillEnabled          bool
	WillTopic            string
	WillPayload          []byte
	WillQos              byte
	WillRetained         bool
	ProtocolVersion      uint
	TLSConfig            map[string]interface{}
	KeepAlive            string
	PingTimeout          string
	ConnectionTimeout    string
	MaxReconnectInterval string
	AutoReconnect        bool
	ConnectRetryInterval string
	ConnectRetry         bool
	WriteTimeout         string
	MessageChannelDepth  uint
	HTTPHeaders          map[string][]string
}

func clientOptionsToMap(options clientOptions) map[string]interface{} {
	return map[string]interface{}{
		"servers":              options.Servers,
		"resumeSubs":           options.ResumeSubs,
		"clientId":             options.ClientID,
		"username":             options.Username,
		"password":             options.Password,
		"cleanSession":         options.CleanSession,
		"order":                options.Order,
		"willEnabled":          options.WillEnabled,
		"willTopic":            options.WillTopic,
		"willPayload":          options.WillPayload,
		"willQos":              options.WillQos,
		"willRetained":         options.WillRetained,
		"protocolVersion":      options.ProtocolVersion,
		"tlsConfig":            options.TLSConfig,
		"keepAlive":            options.KeepAlive,
		"pingTimeout":          options.PingTimeout,
		"connectionTimeout":    options.ConnectionTimeout,
		"maxReconnectInterval": options.MaxReconnectInterval,
		"autoReconnect":        options.AutoReconnect,
		"connectRetryInterval": options.ConnectRetryInterval,
		"connectRetry":         options.ConnectRetry,
		"writeTimeout":         options.WriteTimeout,
		"messageChannelDepth":  options.MessageChannelDepth,
		"httpHeaders":          options.HTTPHeaders,
	}
}

func clientOptionsFromMap(values map[string]interface{}) (clientOptions, error) {
	options := clientOptions{}
	var err error
	serversArray, err := coerce.ToArray(values["servers"])
	if err != nil {
		return options, err
	}
	servers := make([]string, len(serversArray))
	for idx, sa := range serversArray {
		s, err := coerce.ToString(sa)
		if err != nil {
			return options, err
		}
		servers[idx] = s
	}
	options.Servers = servers

	options.ResumeSubs, err = coerce.ToBool(values["resumeSubs"])
	if err != nil {
		return options, err
	}

	options.ClientID, err = coerce.ToString(values["clientId"])
	if err != nil {
		return options, err
	}

	options.Username, err = coerce.ToString(values["username"])
	if err != nil {
		return options, err
	}

	options.Password, err = coerce.ToString(values["password"])
	if err != nil {
		return options, err
	}

	options.CleanSession, err = coerce.ToBool(values["cleanSession"])
	if err != nil {
		return options, err
	}

	options.Order, err = coerce.ToBool(values["order"])
	if err != nil {
		return options, err
	}

	options.WillEnabled, err = coerce.ToBool(values["willEnabled"])
	if err != nil {
		return options, err
	}

	options.WillTopic, err = coerce.ToString(values["willTopic"])
	if err != nil {
		return options, err
	}

	options.WillPayload, err = coerce.ToBytes(values["willPayload"])
	if err != nil {
		return options, err
	}

	qos, err := coerceToInt(values["willQos"])
	if err != nil {
		return options, err
	}
	if qos < 0 || qos > 2 {
		return options, fmt.Errorf("invalid qos: %d", qos)
	}
	options.WillQos = byte(qos)

	options.WillRetained, err = coerce.ToBool(values["willRetained"])
	if err != nil {
		return options, err
	}

	pv, err := coerceToInt(values["protocolVersion"])
	if err != nil {
		return options, err
	}
	if pv < 0 {
		return options, fmt.Errorf("invalid ProtocolVersion: %d", pv)
	}
	options.ProtocolVersion = uint(pv)

	options.TLSConfig, err = coerce.ToObject(values["tlsConfig"])
	if err != nil {
		return options, err
	}

	options.KeepAlive, err = coerce.ToString(values["keepAlive"])
	if err != nil {
		return options, err
	}

	options.PingTimeout, err = coerce.ToString(values["pingTimeout"])
	if err != nil {
		return options, err
	}

	options.ConnectionTimeout, err = coerce.ToString(values["connectionTimeout"])
	if err != nil {
		return options, err
	}

	options.MaxReconnectInterval, err = coerce.ToString(values["maxReconnectInterval"])
	if err != nil {
		return options, err
	}

	options.AutoReconnect, err = coerce.ToBool(values["autoReconnect"])
	if err != nil {
		return options, err
	}

	options.ConnectRetryInterval, err = coerce.ToString(values["connectRetryInterval"])
	if err != nil {
		return options, err
	}

	options.ConnectRetry, err = coerce.ToBool(values["connectRetry"])
	if err != nil {
		return options, err
	}

	options.WriteTimeout, err = coerce.ToString(values["writeTimeout"])
	if err != nil {
		return options, err
	}

	depth, err := coerceToInt(values["messageChannelDepth"])
	if err != nil {
		return options, err
	}
	if depth < 0 {
		return options, fmt.Errorf("invalid MessageChannelDepth: %d", depth)
	}
	options.MessageChannelDepth = uint(depth)

	hs, err := coerce.ToObject(values["httpHeaders"])
	if err != nil {
		return options, err
	}
	headers := map[string][]string{}
	for k, v := range hs {
		arr, err := coerce.ToArray(v)
		if err != nil {
			return options, fmt.Errorf("invalid HTTPHeaders: %#v", hs)
		}
		value := make([]string, len(arr))
		for idx, item := range arr {
			hitem, err := coerce.ToString(item)
			if err != nil {
				return options, fmt.Errorf("invalid HTTPHeaders: %#v", hs)
			}
			value[idx] = hitem
		}
		headers[k] = value
	}
	options.HTTPHeaders = headers

	return options, nil
}

func coerceToInt(val interface{}) (int, error) {
	switch t := val.(type) {
	case uint:
		return int(t), nil
	case int:
		return t, nil
	case int8:
		return int(t), nil
	case uint8:
		return int(t), nil
	case int16:
		return int(t), nil
	case uint16:
		return int(t), nil
	case int32:
		return int(t), nil
	case uint32:
		return int(t), nil
	case uint64:
		return int(t), nil
	case int64:
		return int(t), nil
	case float32:
		return int(t), nil
	case float64:
		return int(t), nil
	case json.Number:
		i, err := t.Int64()
		return int(i), err
	case string:
		return strconv.Atoi(t)
	case bool:
		if t {
			return 1, nil
		}
		return 0, nil
	case nil:
		return 0, nil
	default:
		return 0, fmt.Errorf("unable to coerce %#v to int", val)
	}
}

// Output output
type Output struct {
	Duplicate         bool          `md:"duplicate"`
	Qos               byte          `md:"qos"`
	Retained          bool          `md:"retained"`
	Topic             string        `md:"topic"`
	MessageID         uint16        `md:"messageId"`
	Payload           []byte        `md:"payload"` // The payload received
	IsConnected       bool          `md:"isConnected"`
	IsConnectionOpen  bool          `md:"isConnectionOpen"`
	ConnectionLostErr string        `md:"connectionLostErr"`
	ClientOptions     clientOptions `md:"clientOptions"`
}

// Reply reply
type Reply struct {
	Topic    string `md:"topic"`
	QOS      int    `md:"qos"`
	Retained bool   `md:"retained"`
	Payload  []byte `md:"payload"` // The data to reply with
}

// ToMap output to map
func (o *Output) ToMap() map[string]interface{} {
	return map[string]interface{}{
		"duplicate":        o.Duplicate,
		"qos":              o.Qos,
		"retained":         o.Retained,
		"topic":            o.Topic,
		"messageId":        o.MessageID,
		"payload":          o.Payload,
		"isConnected":      o.IsConnected,
		"isConnectionOpen": o.IsConnectionOpen,
		"clientOptions":    clientOptionsToMap(o.ClientOptions),
	}
}

// FromMap output from map
func (o *Output) FromMap(values map[string]interface{}) error {

	var err error
	o.Duplicate, err = coerce.ToBool(values["duplicate"])
	if err != nil {
		return err
	}

	qos, err := coerceToInt(values["qos"])
	if err != nil {
		return err
	}
	o.Qos = byte(qos)

	o.Retained, err = coerce.ToBool(values["retained"])
	if err != nil {
		return err
	}

	o.Topic, err = coerce.ToString(values["topic"])
	if err != nil {
		return err
	}

	messageID, err := coerceToInt(values["messageId"])
	if err != nil {
		return err
	}
	o.MessageID = uint16(messageID)

	o.Payload, err = coerce.ToBytes(values["payload"])
	if err != nil {
		return err
	}

	o.IsConnected, err = coerce.ToBool(values["isConnected"])
	if err != nil {
		return err
	}

	o.IsConnectionOpen, err = coerce.ToBool(values["isConnectionOpen"])
	if err != nil {
		return err
	}

	obj, err := coerce.ToObject(values["clientOptions"])
	if err != nil {
		return err
	}
	co, err := clientOptionsFromMap(obj)
	if err != nil {
		return err
	}
	o.ClientOptions = co

	return nil
}

// ToMap reply to map
func (r *Reply) ToMap() map[string]interface{} {
	return map[string]interface{}{
		"topic":    r.Topic,
		"qos":      r.QOS,
		"retained": r.Retained,
		"payload":  r.Payload,
	}
}

// FromMap reply from map
func (r *Reply) FromMap(values map[string]interface{}) error {
	var err error

	r.Topic, err = coerce.ToString(values["topic"])
	if err != nil {
		return err
	}

	r.QOS, err = coerce.ToInt(values["qos"])
	if err != nil {
		return err
	}

	r.Retained, err = coerce.ToBool(values["retained"])
	if err != nil {
		return err
	}

	r.Payload, err = coerce.ToBytes(values["payload"])
	if err != nil {
		return err
	}
	return nil
}
